/*
 * @Author: yayalee yangcheng960813@163.com
 * @Date: 2023-05-18 16:26:25
 * @LastEditTime: 2024-07-17 22:20:44
 * @LastEditors: yayalee yangcheng960813@163.com
 * @Description: 
 * @FilePath: \unfinished-challenges\assets\core_cc\ResUtil.ts
 * 可以输入预定的版权声明、个性签名、空行等
 */

import { Asset, AssetManager, ImageAsset, JsonAsset, SpriteAtlas, SpriteFrame, Texture2D, assetManager, path, rect, resources, size } from "cc";

export type TypeConstructor<T = unknown> = new (...args: any[]) => T;
type CustomError = {
    errCode: number; // 错误码
    errMsg: string; // 错误信息
}
type FailFunction = (err: CustomError) => void;

/**
 * @author yayalee
 * @description 资源加载模块
 */
export default class ResUtil {

    /**
     * @description 原生加载资源
     * @param object {url: 远程地址 option: 参数类型}
     * @returns 
     * 
     * @example
     * ResUtil.loadRemote({url:"https://xxxx.xxxx.xxxx/bgm.mp3"}).then((res)=>{});
     * ResUtil.loadRemote({url:"https://xxxx.xxxx.xxxx/bgm.mp3",success:(res)=>{}});
     */
    static loadRemote<T extends Asset>(
        object: {
            url: string,
            option?: any,
        }
    ): Promise<T>
    static loadRemote<T extends Asset>(
        object: {
            url: string,
            option?: any,
            success?: Function,
            fail?: FailFunction,
            complete?: Function
        }
    ): void
    static loadRemote<T extends Asset>(
        object: {
            url: string,
            option?: any,
            success?: Function,
            fail?: FailFunction,
            complete?: Function
        }
    ): Promise<T> | void {
        if (null == object.option) {
            object.option = {};
        }
        const { url, option, success, fail, complete } = object;
        if (success) {
            this._loadRemote(url, option, success, fail, complete);
        } else {
            return new Promise((resolve, reject) => {
                this._loadRemote(url, option, resolve, reject);
            });
        }
    }

    private static _loadRemote<T extends Asset>(url: string, option: any, success?: Function, fail?: FailFunction, complete?: Function) {
        assetManager.loadRemote(url, option, (err: Error | null, asset: T) => {
            if (err) {
                fail && fail({ errCode: -1, errMsg: err.message });
            } else {
                success && success(asset);
            }
            complete && complete();
        });
    }

    /**
     * @description 加载多个资源
     * @param option 
     * @returns 
     * @example
     * ResUtil.loadAssetAny({requests:[{ path: "ab:home/prefab/HomeDialog" }]}).then(res=>{});
     * ResUtil.loadAssetAny({requests:[{ path: "ab:home/prefab/HomeDialog" ],success:(res)=>{}}});
     * ResUtil.loadAssetAny({requests:[{ path: "prefab/HomeDialog" , bundleName:"home" }]}).then(res=>{});
     * ResUtil.loadAssetAny({requests:[{ path: "prefab/HomeDialog" , bundleName:"home"], success:()=>{}}});
     */
    static loadAssetAny(option: {
        requests: {
            path: string,
            type: typeof Asset,
            bundleName?: string,
            bundle?: AssetManager.Bundle,
            progress?: (progress: string, finished: number, total: number) => void
        }[],
        /**完成回调 */
        success?: Function,
        /**失败回调 */
        fail?: FailFunction,
        /**不论完成/失败 都会走的回调 */
        complete?: Function,
        /**总加载进度 */
        totalProgress?: (progress: string, finished: number, total: number) => void
    }) {
        const all = [];
        const length = option.requests.length;
        let finshedNum = 0;
        for (let i = 0; i < length; i++) {
            let request = option.requests[i];
            let reqProgress = request.progress;
            request.progress = (progress: string, finished: number, total: number) => {
                finshedNum++;
                isFunction(reqProgress) && reqProgress!(progress, finished, total);
                option && isFunction(option.totalProgress)
                    && option.totalProgress!(tsx.matchx.toPercentStr(finshedNum / length), finshedNum, length);
            };
            all.push(this.loadAsset(request));
        }
        return Promise.all(all);
    }

    /**
     * 预加载 bundle 中的资源
     * @param option 
     * @example
     * ResUtil.preload({paths:["prefab/HomeDialog","prefab/GameDialog"], bundleName:"bundle", type:Prefab });
     */
    static preload(option: {
        paths: string | string[],
        bundleName?: string,
        bundle?: AssetManager.Bundle,
        type: typeof Asset,
        progress?: (progress: string, finished: number, total: number) => void,
        complete?: Function,
    }): void {
        const { paths, bundleName, bundle, type, progress, complete } = option;
        this.loadBundle({
            bundle: bundle,
            bundleName: bundleName
        }).then((asBundle) => {
            asBundle.preload(paths, type, (finished: number, total: number) => {
                isFunction(progress)
                    && progress!(tsx.matchx.toPercentStr(finished / total), finished, total);
            }, (err: Error | null) => {
                isFunction(complete) && complete!();
            });
        }).catch(() => { });
    }

    /**
     * 同步获取资源 从bundle获取已经加载好了的资源
     * 所以需要提前给资源预加载好不然 会返回 null
     * @param option 
     * @returns 
     * @example
     * var clip = ResUtil.loadAssetSync({path:ab:bundle/bgm.mp3,type:AudioClip});
     */
    static loadAssetSync<T extends Asset>(
        option: {
            path: string,
            type: TypeConstructor<T>,
            bundleName?: string,
            bundle?: AssetManager.Bundle,
        }
    ): T | null {
        const { path, bundleName } = this.parsePath(option.path);
        const abName = bundleName || option.bundleName;
        var bundle: AssetManager.Bundle = null!;
        if (abName && isString(abName)) {
            bundle = assetManager.getBundle(abName!);
        } else {
            bundle = resources;
        }
        if (!bundle) {
            return null;
        }
        return bundle.get(path, option.type);
    }

    /**
     * 加载资源
     * @param option 
     * @returns 
     * @example
     * ResUtil.loadAsset({ path: "ab:home/prefab/HomeDialog" }).then(res=>{});
     * ResUtil.loadAsset({ path: "ab:home/prefab/HomeDialog" ,success:(res)=>{}});
     * ResUtil.loadAsset({ path: "prefab/HomeDialog" , bundleName:"home" }).then(res=>{});
     * ResUtil.loadAsset({ path: "prefab/HomeDialog" , bundleName:"home", success:()=>{}});
     */
    static loadAsset<T extends Asset>(
        option: {
            path: string,
            type: TypeConstructor<T>,
            bundleName?: string,
            bundle?: AssetManager.Bundle,
            progress?: (progress: string, finished: number, total: number) => void
        }
    ): Promise<T>
    static loadAsset<T extends Asset>(
        option: {
            path: string,
            type: TypeConstructor<T>,
            bundleName?: string,
            bundle?: AssetManager.Bundle,
            success?: (asset: T) => void,
            fail?: FailFunction,
            complete?: Function,
            progress?: (progress: string, finished: number, total: number) => void
        }
    ): void
    static loadAsset<T extends Asset>(
        option: {
            path: string,
            type: TypeConstructor<T>,
            bundleName?: string,
            bundle?: AssetManager.Bundle,
            success?: (asset: T) => void,
            fail?: FailFunction,
            complete?: Function,
            progress?: (progress: string, finished: number, total: number) => void
        }
    ): Promise<T> | void {
        const { path, bundleName } = this.parsePath(option.path);
        const abName = bundleName || option.bundleName;
        if (option.success) {
            this._loadAsset(path, option.type, option.bundle, abName, option.success, option.fail, option.complete, option.progress);
            return;
        }
        return new Promise((resolve, reject) => {
            this._loadAsset(
                path,
                option.type,
                option.bundle,
                abName,
                resolve as any,
                reject,
                option.complete,
                option.progress
            );
        });
    }

    private static _loadAsset<T extends Asset>(
        path: string,
        type: TypeConstructor<T>,
        bundle?: AssetManager.Bundle,
        bundleName?: string,
        success?: (asset: T) => void,
        fail?: FailFunction,
        complete?: Function,
        progress?: (progress: string, finished: number, total: number) => void
    ) {
        this.loadBundle({
            bundle: bundle,
            bundleName: bundleName,
            success: (bundle) => {
                const asset: T | null = bundle.get(path, type);
                if (null != asset) {
                    success && success(asset)
                    complete && complete();
                    return;
                }
                bundle.load(path, type,
                    (finished: number, total: number, item: AssetManager.RequestItem) => {
                        isFunction(progress) && progress!(
                            tsx.matchx.toPercentStr(finished / total),
                            finished,
                            total
                        );
                    },
                    (err, asset: T) => {
                        if (err) {
                            fail && fail({ errCode: -1, errMsg: err.message });
                            complete && complete();
                            return;
                        }
                        success && success(asset)
                        complete && complete();
                    });
            },
            fail: (err) => {
                fail && fail(err);
                complete && complete();
            }
        });
    }

    /**
     * 加载bundle
     * @param name 
     * @returns 
     * @example
     * ResUtil.loadBundle({bundleName:"bundle"}).then((bundle)=>{});
     * ResUtil.loadBundle({bundleName:"bundle",success:(bundle)=>{}});
     * ResUtil.loadBundle({bundle:bundle}).then((bundle)=>{});
     */
    static loadBundle(
        option: {
            bundleName?: string,
            bundle?: AssetManager.Bundle,
            success?: (bundle: AssetManager.Bundle) => void,
            fail?: FailFunction,
            complete?: Function
        }
    ): Promise<AssetManager.Bundle>
    static loadBundle(
        option: {
            bundleName?: string,
            bundle?: AssetManager.Bundle,
            success?: (bundle: AssetManager.Bundle) => void,
            fail?: FailFunction,
            complete?: Function
        }
    ): void
    static loadBundle(
        option: {
            bundleName?: string,
            bundle?: AssetManager.Bundle,
            success?: (bundle: AssetManager.Bundle) => void,
            fail?: FailFunction,
            complete?: Function
        }
    ): Promise<AssetManager.Bundle> | void {
        if (option.success) {
            this._loadBundle(option.bundle, option.bundleName, option.success, option.fail, option.complete);
            return;
        }
        return new Promise((resolve, reject) => {
            this._loadBundle(option.bundle, option.bundleName, resolve, reject);
        });
    }

    private static _loadBundle(
        bundle?: AssetManager.Bundle,
        bundleName?: string,
        success?: (bundle: AssetManager.Bundle) => void,
        fail?: FailFunction,
        complete?: Function,
    ) {
        if (!bundle) {
            if (bundleName && isString(bundleName)) {
                bundle = assetManager.getBundle(bundleName!)!;
            } else {
                bundle = resources;
            }
        }
        if (bundle) {
            success && success(bundle);
            complete && complete();
            return
        }
        assetManager.loadBundle(bundleName!, (err, bundle) => {
            if (err) {
                fail && fail({ errCode: -1, errMsg: err.message });
                complete && complete();
                return;
            }
            success && success(bundle);
            complete && complete();
        });
    }

    /**
     * 预加载目录
     * @param option 
     * @returns 
    */
    static preloadDir(
        option: {
            dir: string,
            bundleName?: string,
            bundle?: AssetManager.Bundle,
            complete?: Function,
            progress?: (progress: string, finished: number, total: number) => void
        }
    ): void {
        const { dir, bundleName, bundle, complete, progress } = option;
        this.loadBundle({
            bundle: bundle,
            bundleName: bundleName,
        }).then((bundle) => {
            bundle.preloadDir(
                dir,
                (finished: number, total: number) => {
                    progress && progress(tsx.matchx.toPercentStr(finished / total), finished, total)
                },
                () => {
                    complete && complete();
                }
            );
        });
    }

    /**
     * 加载目录 
     * @param option 
     *  type 不传则加载文件夹下面所有文件
     * @returns 
     * @example
     * ResUtil.loadDir({bundleName:"bundle",dir:"prefab",type:cc.Prefab}).then((asset:[])=>{});
     * ResUtil.loadDir({bundleName:"bundle",dir:"/",success:(asset:[])=>{}})
     */
    static loadDir<T>(
        option: {
            dir: string,
            type?: TypeConstructor<T>,
            bundleName?: string,
            bundle?: AssetManager.Bundle,
            progress?: (progress: string, finished: number, total: number) => void
        }
    ): Promise<Asset[]>
    static loadDir<T>(
        option: {
            dir: string,
            type?: TypeConstructor<T>,
            bundleName?: string,
            bundle?: AssetManager.Bundle,
            success?: (assets: Asset[]) => void,
            fail?: FailFunction,
            complete?: Function,
            progress?: (progress: string, finished: number, total: number) => void
        }
    ): void
    static loadDir<T>(
        option: {
            dir: string,
            type?: TypeConstructor<T>,
            bundleName?: string,
            bundle?: AssetManager.Bundle,
            success?: (assets: Asset[]) => void,
            fail?: FailFunction,
            complete?: Function,
            progress?: (progress: string, finished: number, total: number) => void
        }
    ): Promise<Asset[]> | void {
        if (option.success) {
            this._loadDir(option.dir, option.bundle, option.bundleName,
                option.success, option.fail, option.complete, option.progress);
            return;
        }
        return new Promise((resolve, reject) => {
            this._loadDir(option.dir, option.bundle, option.bundleName,
                resolve, reject, option.complete, option.progress);
        });
    }

    private static _loadDir(
        dir: string,
        bundle?: AssetManager.Bundle,
        bundleName?: string,
        success?: (assets: Asset[]) => void,
        fail?: FailFunction,
        complete?: Function,
        progress?: (progress: string, finished: number, total: number) => void
    ) {
        this.loadBundle({
            bundle: bundle,
            bundleName: bundleName,
            success: (bundle) => {
                bundle.loadDir(dir, (finished: number, total: number, item: AssetManager.RequestItem) => {
                    isFunction(progress) && progress!(tsx.matchx.toPercentStr(finished / total), finished, total);
                }, (err, assets) => {
                    if (err) {
                        fail && fail({ errCode: -1, errMsg: err.message });
                        complete && complete();
                        return;
                    }
                    success && success(assets)
                    complete && complete();
                });
            },
            fail: (err) => {
                fail && fail(err);
                complete && complete();
            }
        });
    }


    /**
    * 加载远程图集
    * @param url 
    * @returns 
    */
    static loadRemotePlist(url: string): Promise<SpriteAtlas> {
        return new Promise((resolve, reject) => {
            this.loadRemote<JsonAsset>({
                url
            }).then((plist: JsonAsset) => {
                const asset = plist._nativeAsset;
                let texture = asset.metadata.realTextureFileName || asset.metadata.textureFileName;
                texture = path.join(path.dirname(url), texture);
                this.loadRemote<ImageAsset>({
                    url: texture
                }).then((image: ImageAsset) => {
                    const texture = new Texture2D();
                    const sa = new SpriteAtlas();
                    texture.image = image;
                    const frames = asset.frames;
                    const sfs = sa.spriteFrames;
                    const plistRegex = /[,{}\s]+/;
                    const tmpRect = rect();
                    const tmpSize = size();
                    for (const key in frames) {
                        const sf = new SpriteFrame();
                        const frame = frames[key];
                        sf.texture = texture;
                        let tmp: string[] = frame.frame.split(plistRegex, 5);
                        sf.rect = tmpRect.set(parseInt(tmp[1]), parseInt(tmp[2]), parseInt(tmp[3]), parseInt(tmp[4]));
                        tmp = frame.offset.split(plistRegex, 3);
                        sf.offset = tmpRect.set(parseInt(tmp[1]), parseInt(tmp[2]));
                        tmp = frame.sourceSize.split(plistRegex, 3);
                        sf.originalSize = tmpSize.set(parseInt(tmp[1]), parseInt(tmp[2]));
                        sf.rotated = frame.rotated;
                        sfs[key.slice(0, -4)] = sf; //key需要去掉后缀.png
                    }
                    resolve(sa);
                }).catch((err) => {
                    reject(err);
                });
            }).catch((err) => {
                reject(err);
            });
        });
    }

    /**加载精灵 */
    static loadSpriteFrame(
        option: {
            path: string,
            bundleName?: string,
            bundle?: AssetManager.Bundle,
            success?: (asset: SpriteFrame) => void,
            fail?: FailFunction,
            complete?: Function,
            progress?: (progress: string, finished: number, total: number) => void
        }
    ): Promise<SpriteFrame> {
        const { path, bundleName } = this.parsePath(option.path);
        const abName = bundleName || option.bundleName;
        if (option.success) {
            this._loadAsset(path, SpriteFrame, option.bundle, abName, option.success, option.fail, option.complete, option.progress);
            return;
        }
        return new Promise((resolve, reject) => {
            this._loadAsset(
                path,
                SpriteFrame,
                option.bundle,
                abName,
                resolve as any,
                reject,
                option.complete,
                option.progress
            );
        });
    }


    /** asset bundle路径校验 */
    static BUNDLE_CHECK = "ab:";
    /**
     * 资源路径解析
     * @param url 
     */
    public static parsePath(path: string): { bundleName?: string, path: string } {
        if (path.startsWith(this.BUNDLE_CHECK)) {
            let loadUrl = path.substring(this.BUNDLE_CHECK.length);
            let idx = loadUrl.indexOf("/");
            let bundle = loadUrl.substring(0, idx);
            loadUrl = loadUrl.substring(idx + 1);
            return { bundleName: bundle, path: loadUrl };
        } else {
            return { path };
        }
    }

}